<?php

namespace Yolo\Commands;

use Hyperf\Command\Annotation\Command;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\Stringable\Str;
use Hyperf\Support\Filesystem\Filesystem;
use PharData;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use Symfony\Component\Console\Input\InputOption;
use Yolo\Di\DI;
use Yolo\Packages\PackageManager;
use Yolo\Service\ApplicationContainer;
use Yolo\Utils\Network;

#[Command]
class PublishPackage extends HyperfCommand
{
    private ApplicationContainer $applicationContainer;

    private string $schemeDir = 'Scheme';

    private array $methods = [];

    private string $packageName = '';

    private string $version = '';

    private array $packageServer = [];

    private PackageManager $packageManager;

    public function __construct(protected ContainerInterface $container)
    {
        parent::__construct('package:publish');

        $this->applicationContainer = DI::use(ApplicationContainer::class);
        $this->packageManager = DI::use(PackageManager::class);
    }

    public function configure(): void
    {
        parent::configure();
        $this->setDescription('Publish a package.');
        $this->addOption('preview', null, null, 'Preview the package and not publish the package.');
        $this->addOption('ver', null, InputOption::VALUE_REQUIRED, 'The package version.');
    }

    public function handle(): void
    {
        $fileSystem = new Filesystem();

        try {

            $this->publish();
        } catch (\Exception $e) {

            $this->error($e->getMessage());
        } finally {

            $fileSystem->deleteDirectory(BASE_PATH . '/.publish');
        }
    }

    public function publish(): void
    {
        if (!$this->input->getOption('ver')) {
            $this->error('Please specify the package version.');
            return;
        }

        $version = $this->input->getOption('ver');
        if (!$this->isValidVersion($version)) {
            $this->error('Invalid version format.');
            return;
        }

        $this->packageServer = $this->applicationContainer->getConfig()->get('packageServer');
        if (!$this->packageServer || !$this->packageServer['host']) {
            throw new \Exception('Package server is not configured.');
        }

        $this->version = $version;

        $serviceName = $this->applicationContainer->getAppScheme()->getServiceName();

        $this->packageName = $this->packageManager->getPackageFile()['name'] ?? '';
        if (!$this->packageName) {
            $this->error('Package name is not configured.');
            return;
        }

        $schemePath = BASE_PATH . '/app/' . $this->schemeDir;

        $fileSystem = new Filesystem();
        $files = $fileSystem->files($schemePath);

        $templateArgs = [
            'serviceName' => $serviceName,
            'files' => [],
            'methods' => [],
        ];

        foreach ($files as $file) {

            if ($file->getExtension() !== 'php') {
                continue;
            }

            $this->info("Collecting {$file->getPath()}/{$file->getFilename()}...");

            $class = $file->getBasename('.php');
            $reflection = $this->resolveClass("App\\Scheme\\$class");
            if (!$reflection->isInterface()) {
                $this->error("Warning: $class is not an interface, skipped.");
                continue;
            }

            $templateArgs['files'][] = "{$file->getPath()}/{$file->getFilename()}";

            $methods = $this->resolveMethods($reflection);

            foreach ($methods as $method) {

                if (in_array($method->getName(), $this->methods)) {

                    $this->error("Error: Rpc method '{$method->getName()}' duplicated.");
                    return;
                }

                $this->methods[] = $method->getName();
                $doc = $method->getDocComment();

                $parameters = $method->getParameters();

                $params = [];
                foreach ($parameters as $param) {
                    $params[] = [
                        'type' => $param->getType()?->getName() ?? '',
                        'name' => $param->getName(),
                        'isOptional' => $param->isOptional(),
                        'default' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : ($param->isDefaultValueConstant() ? $param->getDefaultValueConstantName() : null),
                    ];
                }

                $templateArgs['methods'][] = [
                    'name' => $method->getName(),
                    'doc' => $doc,
                    'params' => $params,
                    'returnType' => $method->getReturnType()?->getName() ?? 'void',
                ];
            }
        }

        $tarFile = $this->renderTemplate($templateArgs);

        $this->info("Publishing package $this->packageName $this->version". '...');

        $packageJson = $this->packageManager->getPackageFile();

        $this->publishPackage($tarFile, [
            'name' => $this->packageName,
            'description' => $packageJson['description'] ?? '',
            'version' => $this->version,
            'context' => json_encode([
                'serviceName' => $serviceName,
                'repository' => $packageJson['repository'] ?? [],
                'keywords' => $packageJson['keywords'] ?? [],
                'license' => $packageJson['license'] ?? '',
                'author' => $packageJson['author'] ?? '',
            ]),
        ]);

        $this->info('Package published successfully.');
    }
    private function renderTemplate(array $args): string
    {
        $templateFile = BASE_PATH . '/.publish';

        $fileSystem = new Filesystem();
        $fileSystem->deleteDirectory($templateFile);
        $fileSystem->makeDirectory($templateFile);
        $fileSystem->makeDirectory($templateFile . '/tmp');
        $fileSystem->makeDirectory($templateFile . '/tmp/Scheme');

        foreach ($args['files'] as $file) {
            $this->info("Copying files $file...");
            $fileSystem->copy($file, $templateFile . '/tmp/Scheme/' . basename($file));
            $this->updateNamespace($templateFile . '/tmp/Scheme/' . basename($file));
        }

        $apiFile = "{$templateFile}/tmp/{$args['serviceName']}.php";

        $templateContent = file_get_contents(__DIR__ . '/tpls/publishPackage/class-apis.php.tpl');

        $templateContent = str_replace('{{serviceName}}', $args['serviceName'], $templateContent);

        $methods = [];
        foreach ($args['methods'] as $method) {

            $upperName = ucfirst($method['name']);
            $doc = $this->addExceptionTag($method['doc']);

            $methods[] = <<<TEXT
    {$doc}
    public function {$method['name']}(array \$args): ResponseInterface
    {
        return \$this->client->invoke('{$upperName}', \$args);
    }

TEXT;
        }

        $templateContent = str_replace('{{methods}}', implode("\n", $methods), $templateContent);

        file_put_contents($apiFile, $templateContent);

        return $this->tar("{$templateFile}/tmp");
    }

    /**
     * @param string $class
     * @return ReflectionClass
     * @throws ReflectionException
     */
    private function resolveClass(string $class): ReflectionClass
    {
        return new ReflectionClass($class);
    }

    /**
     * @param ReflectionClass $reflection
     * @return array<ReflectionMethod>
     */
    private function resolveMethods(ReflectionClass $reflection): array
    {
        return $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
    }

    /**
     * 给注释添加异常注释
     * @param string $doc
     * @return string
     */
    private function addExceptionTag(string $doc): string
    {
        $docLines = explode("\n", $doc);
        $exceptions = ['GuzzleException'];
        $exceptionsNum = count($exceptions);
        foreach ($docLines as $line) {
            if (str_contains($line, '@throws')) {

                for ($i = 0; $i < $exceptionsNum; $i++) {

                    if (str_contains($line, $exceptions[$i])) {
                        unset($exceptions[$i]);
                    }
                }
            }
        }


        foreach ($exceptions as $exception) {
            $lines = count($docLines);

            array_splice($docLines, $lines - 1, 0, "     * @throws $exception");
        }

        return implode("\n", $docLines);
    }

    /**
     * 更新命名空间
     * @param string $path
     * @return void
     */
    private function updateNamespace(string $path): void
    {
        $content = file_get_contents($path);
        $content = str_replace(
            'namespace App\Scheme',
            'namespace App\Package\\' . $this->applicationContainer->getAppScheme()->getServiceName() . '\Scheme',
            $content
        );
        file_put_contents($path, $content);
    }

    private function tar(string $path): string
    {
        $outputTarFile = BASE_PATH . "/.publish/" . md5($this->getFullPackageName()) . '.tar';

        // 创建一个新的PharData对象
        $phar = new PharData($outputTarFile);

        // 将整个目录添加到tar归档中
        $phar->buildFromDirectory($path);

        return $outputTarFile;
    }

    private function isValidVersion(string $version): bool
    {
        return preg_match('/^\d+\.\d+\.\d+$/', $version) === 1;
    }

    private function getFullPackageName(): string
    {
        return $this->packageName . '-' . $this->version;
    }

    private function publishPackage(string $tarFile, array $args): void
    {
        $this->info("Uploading package $tarFile ...");
        Network::uploadFile($tarFile, [
            'serverUrl' => $this->packageServer['host'] . '/api/package/publish',
            'field' => 'file',
            'args' => $args,
        ]);
    }
}